import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author James Marquardt
 * @author Matt Adams
 * @author Justin Wong
 */
public class NodeActual implements Node {   
    
    public static final String NAME_IN_REGISTRY = "endpoint:";
    
    /**
     * Host and port of this node.
     */
    private HostPort nodeHostPort;
    /**
     * Host and port of node's successor.
     */
    private HostPort successorHostPort;
    /**
     * Host and port of node's predecessor.
     */
    private HostPort predecessorHostPort;
    /**
     * The id of this node.
     */
    private int nodeID;
    /**
     * The maximum number of nodes in the chord this node belongs to.
     */
    private int maxNodesInChord;
    /**
     * The finger table of this node.
     */
    private Map<Integer, HostPort> fingerTable;

    
    /**
     * Constructor for a Node
     * @param remoteService The node registry.
     * @param hostPort the HostPort of this node.
     */
    @SuppressWarnings("unchecked")
	public NodeActual(final RemoteNodeRegistry remoteService,
                      final HostPort hostPort) {
        
    	fingerTable = new HashMap<Integer, HostPort>();
    	
    	Map<Integer, HostPort> temptable = new HashMap<Integer, HostPort>();
    	
    	this.nodeHostPort = hostPort;
    	try {
            //Unpack registration info.
    		RegistrationPacket packet = remoteService.registerNode(this.nodeHostPort);
            this.successorHostPort = packet.successorNodeReference;
            this.predecessorHostPort = packet.predecessorNodeReference;
            this.maxNodesInChord = packet.m_value;
            this.nodeID = packet.node_id; 
            temptable = (HashMap<Integer, HostPort>) deserialize(packet.fingertable);

            messageToFile("Node ID: " + nodeID);
            messageToFile("Location: " + nodeHostPort);
            messageToFile("Successor: " + this.successorHostPort);
            messageToFile("Predecessor: " + this.predecessorHostPort);
             
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
			// TODO Auto-generated catch block
			messageToFile(e.getLocalizedMessage());
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			messageToFile(e.getLocalizedMessage());
		}
    	
    	//Initialize keys in finger table.
    	for(int i=0; i<maxNodesInChord; i++) {
    		int fingerID = nodeID+(int)Math.pow(2,i);
    		if(fingerID>=Math.pow(2, maxNodesInChord)) {
    			fingerID-=Math.pow(2, maxNodesInChord);
    		}
    		fingerTable.put(fingerID, temptable.get(fingerID));
    	}
    	
    	messageToFile("finger table keys: ");
    	for(int i : fingerTable.keySet()) {
    		messageToFile(i + " " + fingerTable.get(i) + " ");
    	}
    	messageToFile("\n");
    	
    	updatePredecessor();
    	updateSuccessor();
    	updateFingers();
    	//listen();
    	
    }
    
    /**
     * Method to deserialize passed byte arrays.
     * @param b
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private static Object deserialize(byte[] b) throws IOException, ClassNotFoundException {
    	ByteArrayInputStream ba = new ByteArrayInputStream(b);
    	ObjectInputStream oba = new ObjectInputStream(ba);
    	return oba.readObject();
    }
    
    /**
     * Have node wait for communication.
     */
    public void run() {
    	ServerSocket in_socket = null;
    	try {
    		in_socket = new ServerSocket(this.nodeHostPort.getPort());
    	} catch (IOException e) {
    		// TODO Auto-generated catch block
    		Log.err(e.getLocalizedMessage());
    	}

    	while(true) {
    		String message;
    		try{
    			Socket connect = in_socket.accept();
    			BufferedReader incoming =
    					new BufferedReader(new InputStreamReader(connect.getInputStream()));

    			message = incoming.readLine();
    			
    			//Parse command
    			if(message.equals("updatepredecessor")) {
    				               
    				String host = incoming.readLine();
    				int port = Integer.parseInt(incoming.readLine());

    				this.predecessorHostPort = new HostPort(host, port);
    				
    				messageToFile("predecessor updated: " + this.predecessorHostPort);
    				
    			} else if(message.equals("updatesuccessor")) {
    				String host = incoming.readLine();
    				int port = Integer.parseInt(incoming.readLine());

    				this.successorHostPort = new HostPort(host, port);
    				
    				messageToFile("successor updated: " + this.successorHostPort);
    			} else if(message.equals("updatefingers")) {
    				updateFingers(incoming);
    			}
    				
    		} catch (IOException e) {
    			Log.err(e.getLocalizedMessage());
    		}
    	}
    }
    
    /**
     * Signal other nodes to update finger tables if needed.
     */
    private void updateFingers() {
    	if(!(this.nodeHostPort.equals(this.successorHostPort) &&
    			this.nodeHostPort.equals(this.successorHostPort))) {

    		try {
    			Socket updateSocket = new Socket(this.successorHostPort.getHost(),
    					this.successorHostPort.getPort());
    			DataOutputStream out = new DataOutputStream(updateSocket.getOutputStream());
    			out.writeBytes("updatefingers" + '\n' + this.nodeID + 
    					'\n' + this.nodeHostPort.getHost() + '\n' + this.nodeHostPort.getPort() + '\n');
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    }

    /**
     * Continue propagation of finger table updates.
     * 
     * @param the_incoming The message passed.
     */
    private void updateFingers(BufferedReader the_incoming) {

    	try{
    		int incoming_id = Integer.parseInt(the_incoming.readLine());
    		String the_host=the_incoming.readLine();
    		int the_port=Integer.parseInt(the_incoming.readLine());
    		HostPort finger = new HostPort(the_host, the_port);
    
    		if((incoming_id) != this.nodeID) {
    			if(fingerTable.containsKey(incoming_id)) {
    				
    				fingerTable.put(incoming_id, finger);
    			
    				messageToFile("updated table entry: " + incoming_id + " " + fingerTable.get(incoming_id));
    			}
    			
    				if(!(finger.equals(this.successorHostPort))) {
    				Socket updateSocket = new Socket(this.successorHostPort.getHost(),
    						this.successorHostPort.getPort());
    				DataOutputStream out = new DataOutputStream(updateSocket.getOutputStream());
    				out.writeBytes("updatefingers" + '\n' + incoming_id + 
    						'\n' + the_host + '\n' + the_port + '\n');
    				}
    					
    		}
    
    	} catch (IOException e) {
    		Log.err(e.getLocalizedMessage());
    	}

    }

    /**
     * Update the predecessor of this node.
     */
    private void updatePredecessor() {
    	if(!(this.nodeHostPort.equals(this.predecessorHostPort) &&
    			this.nodeHostPort.equals(this.predecessorHostPort))){
    		try {
    			Socket updateSocket = new Socket(this.predecessorHostPort.getHost(),
    					this.predecessorHostPort.getPort());
    			DataOutputStream out = new DataOutputStream(updateSocket.getOutputStream());
    			out.writeBytes("updatesuccessor" + '\n' + this.nodeHostPort.getHost() + 
    					'\n' + this.nodeHostPort.getPort() + '\n');
    		} catch (IOException e) {
    			Log.err(e.getLocalizedMessage());
    		}
    	}
    }
    
    /**
     * Update the successor of this node.
     */
    private void updateSuccessor() {
    	if(!(this.nodeHostPort.equals(this.successorHostPort) &&
    			this.nodeHostPort.equals(this.successorHostPort))){
    		try {
    			Socket updateSocket = new Socket(this.successorHostPort.getHost(),
    					this.successorHostPort.getPort());
    			DataOutputStream out = new DataOutputStream(updateSocket.getOutputStream());
    			out.writeBytes("updatepredecessor" + '\n' + this.nodeHostPort.getHost() + 
    					'\n' + this.nodeHostPort.getPort() + '\n');
    		} catch (IOException e) {
    			Log.err(e.getMessage());
    		}
    	}
    }

    @Override
    public String remoteToString() {
    	final StringBuilder sb = new StringBuilder();
    	sb.append("Node ID: " + this.nodeID);
    	sb.append("\nsuccessor: " + successorHostPort.toString());
    	sb.append("\npredecessor: " + predecessorHostPort.toString());
    	sb.append("\n");
    	return sb.toString();
    }

    @Override
    public int remoteHashCode() {
    	final int prime = 31;
    	int result = 1;
    	result = prime * result + nodeID;
    	return result;
    }

    @Override
    public boolean remoteEquals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        NodeActual other = (NodeActual) obj;
        if (nodeID != other.nodeID)
            return false;
        return true;
    }
    
    /**
     * Prints message to log file.
     * 
     * @param the_message
     */
    private void messageToFile(String the_message) {
    	try {
    	Log.toFile(the_message, nodeID);
    	} catch (FileNotFoundException e) {
    		Log.err("could not create or find log file for node " + nodeID);
    	}
    }
}
